package response

import (
	"context"
	"encoding/xml"
	"fmt"
	"gitee.com/zackeus/go-boot/common/constants/codes"
	"gitee.com/zackeus/go-boot/common/constants/net/headers"
	"gitee.com/zackeus/go-boot/tools/errorx"
	"gitee.com/zackeus/go-zero/core/logx"
	"gitee.com/zackeus/go-zero/rest/httpx"
	"gitee.com/zackeus/goutil/strutil"
	"github.com/beevik/etree"
	rpccodes "google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"net/http"
)

type Response struct {
	Code int    `json:"code"`
	Msg  string `json:"msg"`
}

type XmlResponse struct {
	Code int    `json:"code"`
	Msg  string `json:"msg"`
}

func (x *XmlResponse) MarshalToXml() ([]byte, error) {
	doc := etree.NewDocument()
	doc.CreateProcInst("xml", `version="1.0" encoding="UTF-8"`)
	response := doc.CreateElement("response")
	code := response.CreateElement("code")
	code.CreateText(strutil.SafeString(x.Code))
	msg := response.CreateElement("msg")
	msg.CreateText(x.Msg)

	doc.Indent(1)
	return doc.WriteToBytes()
}

// Ok writes HTTP 200 OK into w.
func Ok(w http.ResponseWriter) {
	httpx.Ok(w)
}

// OkJson 成功响应
func OkJson(ctx context.Context, w http.ResponseWriter, v any) {
	httpx.OkJsonCtx(ctx, w, v)
}

// WriteJsonCtx 响应json
func WriteJsonCtx(ctx context.Context, w http.ResponseWriter, code int, v any) {
	httpx.WriteJsonCtx(ctx, w, code, v)
}

// OkXml writes v into w with 200 OK.
func OkXml(ctx context.Context, w http.ResponseWriter, v any) {
	WriteXmlCtx(ctx, w, http.StatusOK, v)
}

// WriteXmlCtx writes v as xml string into w with code.
func WriteXmlCtx(ctx context.Context, w http.ResponseWriter, code int, v any) {
	if err := doWriteXml(w, code, v); err != nil {
		logx.WithContext(ctx).Error(err)
	}
}

// ErrorJson json 异常响应
func ErrorJson(ctx context.Context, w http.ResponseWriter, err error) {
	ErrorJsonWithStatus(ctx, http.StatusOK, w, err)
}

func ErrorJsonWithStatus(ctx context.Context, status int, w http.ResponseWriter, err error) {
	errCode, errMsg := doParseError(err)
	logx.WithContext(ctx).Errorf("【RESPONSE-ERR】 : %+v ", err)
	httpx.WriteJsonCtx(ctx, w, status, &Response{Code: errCode, Msg: errMsg})
}

// ErrorXml xml 异常响应
func ErrorXml(ctx context.Context, w http.ResponseWriter, err error) {
	errCode, errMsg := doParseError(err)
	logx.WithContext(ctx).Errorf("【RESPONSE-ERR】 : %+v ", err)

	WriteXmlCtx(ctx, w, http.StatusOK, &XmlResponse{Code: errCode, Msg: errMsg})
}

func doParseError(err error) (errCode int, errMsg string) {
	/* 获取 CauseErr */
	causeErr := errorx.Cause(err)
	if e, ok := causeErr.(errorx.ErrorR); ok {
		/* 自定义CodeError */
		errCode = e.Code()
		errMsg = e.Error()
	} else if s, ok := status.FromError(causeErr); ok {
		/* grpc err错误 */
		switch s.Code() {
		case rpccodes.NotFound, rpccodes.DataLoss:
			errCode = codes.NoExist
		case rpccodes.InvalidArgument, rpccodes.AlreadyExists:
			errCode = codes.UnprocessableEntity
		case rpccodes.PermissionDenied:
			errCode = codes.Forbidden
		default:
			errCode = codes.ServerErr
		}

		if !strutil.IsEmpty(s.Message()) && strutil.IsNotBlank(s.Message()) {
			errMsg = s.Message()
		} else {
			errMsg = "rpc server exception"
		}
	} else {
		errCode = codes.ServerErr
		if !strutil.IsEmpty(causeErr.Error()) && strutil.IsNotBlank(causeErr.Error()) {
			errMsg = causeErr.Error()
		} else {
			errMsg = "api server exception"
		}
	}
	return errCode, errMsg
}

func doWriteXml(w http.ResponseWriter, code int, v any) error {
	var (
		bs  []byte
		err error
	)
	if i, ok := v.(interface{ MarshalToXml() ([]byte, error) }); ok {
		bs, err = i.MarshalToXml()
	} else {
		bs, err = xml.Marshal(v)
	}
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return fmt.Errorf("marshal xml failed, error: %w", err)
	}

	w.Header().Set(httpx.ContentType, headers.ApplicationXml)
	w.WriteHeader(code)

	if n, err := w.Write(bs); err != nil {
		// http.ErrHandlerTimeout has been handled by http.TimeoutHandler,
		// so it's ignored here.
		if err != http.ErrHandlerTimeout {
			return fmt.Errorf("write response failed, error: %w", err)
		}
	} else if n < len(bs) {
		return fmt.Errorf("actual bytes: %d, written bytes: %d", len(bs), n)
	}

	return nil
}
